package com.gee.spring.cloud.feign.flow.util;

import com.gee.spring.cloud.feign.flow.binding.FeignFlowProxyFactory;
import com.gee.spring.cloud.feign.flow.binding.annotation.FeignFlow;
import com.gee.spring.cloud.feign.flow.binding.annotation.FlowArg;
import com.gee.spring.cloud.feign.flow.binding.annotation.FeignFlows;
import com.gee.spring.cloud.feign.flow.binding.annotation.NextFeignFlow;
import com.gee.spring.cloud.feign.flow.binding.annotation.TempVar;
import com.gee.spring.cloud.feign.flow.binding.exception.BindingException;
import com.gee.spring.cloud.feign.flow.binding.exception.MethodNotFoundException;
import com.gee.spring.cloud.feign.flow.execute.WorkExecuteDesigner;
import com.gee.spring.cloud.feign.flow.execute.nextwork.Condition;
import com.gee.spring.cloud.feign.flow.execute.nextwork.NextWorkWrapper;
import com.gee.spring.cloud.feign.flow.execute.nextwork.SimpleCondition;
import com.gee.spring.cloud.feign.flow.handler.WorkHandler;
import com.gee.spring.cloud.feign.flow.work.impl.DefaultWork;
import com.gee.spring.cloud.feign.flow.work.impl.EndWork;
import com.gee.spring.cloud.feign.flow.work.impl.InitWork;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * desc:
 *
 * @author gee
 * @since 2021-04-07 11:12:33
 */
public class FlowUtil {

    private static final Logger logger = LoggerFactory.getLogger(FlowUtil.class);

    private static Map<Class<?>, WorkHandler> workHandlerCache = new ConcurrentHashMap<>();

    private static Map<Class<?>, FeignFlowProxyFactory<?>> knownFeignFlowMap = new HashMap<>();

    private static Map<Class<?>, Condition> conditionCache = new ConcurrentHashMap<>();

    private static Map<Method, WorkExecuteDesigner> designerCache = new ConcurrentHashMap<>();

    private static Map<TempVar,Method> calculatorMethodCache = new ConcurrentHashMap<>();

    public static Class[] getArgTypes(FlowArg[] argsAnnotations) {
        if (argsAnnotations.length == 0){
            return null;
        }
        Class[] argTypes = new Class[argsAnnotations.length];
        for (int i = 0; i < argsAnnotations.length; i++) {
            argTypes[i] = argsAnnotations[i].argType();
        }
        return argTypes;
    }

    public static String[] getArgKeys(FlowArg[] argsAnnotations) {
        if (argsAnnotations.length == 0){
            return null;
        }
        String[] args = new String[argsAnnotations.length];
        for (int i = 0; i < argsAnnotations.length; i++) {
            args[i] = argsAnnotations[i].key();
        }
        return args;
    }

    public static Method getCalculatorMethod(TempVar tempVar) {
        if (!calculatorMethodCache.containsKey(tempVar)){
            calculatorMethodCache.computeIfAbsent(tempVar,  varAnnotation-> {
                Class<?> targetClass = tempVar.calculateClass();
                FlowArg[] argAnnotations = tempVar.args();
                Class[] argTypes = FlowUtil.getArgTypes(argAnnotations);
                Method method = ReflectionUtils.findMethod(targetClass, tempVar.methodName(), argTypes);
                if (!checkMethod(method, tempVar)) {
                    return null;
                }else {
                    return method;
                }
            });
        }
        return calculatorMethodCache.get(tempVar);
    }

    private static boolean checkMethod(Method method, TempVar tempVar) {
        if (method == null){
            logger.error("method not found: {}.{}", tempVar.calculateClass().getName(),tempVar.methodName());
            return false;
        }
        if (!Modifier.isStatic(method.getModifiers())){
            logger.error("method should be static: {}.{}", tempVar.calculateClass().getName(),tempVar.methodName());
            return false;
        }

        if (!tempVar.varType().equals(method.getReturnType())){
            logger.error("method's returnType should be same with the varType: {}.{}", tempVar.calculateClass().getName(),tempVar.methodName());
            return false;
        }
        return true;
    }

    public static Condition getCondition(Class<? extends Condition> conditionClass) {
        if (!conditionCache.containsKey(conditionClass)){
            conditionCache.computeIfAbsent(conditionClass, clazz -> {
                try {
                    return (Condition) clazz.newInstance();
                } catch (InstantiationException | IllegalAccessException e) {
                    logger.error("condition class not found: {}", conditionClass.getName());
                }
                return null;
            });
        }
        return conditionCache.get(conditionClass);
    }


    public static WorkHandler getWorkHandler(Class<? extends WorkHandler> handlerClass) {
        if (!workHandlerCache.containsKey(handlerClass)){
            workHandlerCache.computeIfAbsent(handlerClass, clazz -> {
                try {
                    return (WorkHandler) clazz.newInstance();
                } catch (InstantiationException | IllegalAccessException e) {
                    logger.error("condition class not found: {}", handlerClass.getName());
                }
                return null;
            });
        }
        return workHandlerCache.get(handlerClass);
    }

    public static <T> boolean hasFeignFlow(Class<T> type) {
        return knownFeignFlowMap.containsKey(type);
    }

    public static <T> T getFeignFlow(Class<T> type) throws BindingException {
        FeignFlowProxyFactory<T> feignFlowProxyFactory = (FeignFlowProxyFactory<T>) knownFeignFlowMap.get(type);
        if (feignFlowProxyFactory == null) {
            throw new BindingException("Type " + type + " is not known to the FeignFlowCache.");
        } else {
            try {
                return feignFlowProxyFactory.newInstance();
            } catch (Exception var5) {
                throw new BindingException("Error getting FeignFlow instance. Cause: " + var5, var5);
            }
        }
    }

    public static <T> void addFeignFlow(Class<T> type, BeanFactory beanFactory, ThreadPoolExecutor executor) throws Exception{
        if (type.isInterface()) {
            if (hasFeignFlow(type)) {
                throw new BindingException("Type " + type + " is already known to the FeignFlowCache.");
            }
            boolean loadCompleted = false;
            try {
                knownFeignFlowMap.put(type,
                        new FeignFlowProxyFactory<>(type,beanFactory, executor));
                loadCompleted = true;
            } finally {
                if (!loadCompleted) {
                    knownFeignFlowMap.remove(type);
                }
            }
        }
    }

    public static WorkExecuteDesigner getDesigner(Method method, BeanFactory beanFactory) throws MethodNotFoundException {
        if (!designerCache.containsKey(method)){
            FeignFlows annotation = AnnotationUtils.findAnnotation(method, FeignFlows.class);
            if (annotation == null){
                logger.warn("feign flow's method [{}] should be annotated with FeignMethodFlows but not found.", method.getClass().getName() + "." + method.getName());
                return null;
            }
            FeignFlow[] feignFlows = annotation.feignFlows();
            Class<? extends WorkHandler> handlerClass = annotation.workHandlerClass();
            WorkHandler commonWorkHandler = getWorkHandler(handlerClass);
            if (feignFlows.length == 0){
                logger.warn("FeignMethodFlow has no element at  method [{}]", method.getClass().getName() + "." + method.getName());
                return null;
            }
            Map<String, DefaultWork> allWorkMap = new HashMap<>();
            Map<String,DefaultWork> startWorkMap = new HashMap<>();

            EndWork endWork = EndWork.build();
            InitWork initWork = InitWork.build();
            for (FeignFlow feignFlow : feignFlows) {
                Class[] argTypes = getArgTypes(feignFlow.args());

                Object target = beanFactory.getBean(feignFlow.feignClass());
                Method feignMethod = ReflectionUtils.findMethod(target.getClass(), feignFlow.method(),argTypes);
                if (feignMethod == null){
                    throw new MethodNotFoundException(feignFlow.method() + " in class " + feignFlow.feignClass()
                            + "with argTypes " + Arrays.toString(argTypes));
                }
                DefaultWork work = new DefaultWork(feignFlow.feignFlowId(), target, feignMethod,
                        feignFlow.args(), feignFlow.tempVars());
                allWorkMap.put(work.getId(), work);
                startWorkMap.put(work.getId(), work);
                WorkHandler workHandler = getWorkHandler(feignFlow.workHandlerClass());
                workHandler = workHandler == null ? commonWorkHandler : workHandler;
                work.setWorkHandler(workHandler);
                NextFeignFlow[] nextFeignFlows = feignFlow.nextFeignFlows();
                if (nextFeignFlows.length == 0){
                    NextWorkWrapper nextWorkWrapper = NextWorkWrapper.build(endWork, feignFlow.necessaryForEnd(), getCondition(SimpleCondition.class));
                    work.getWorkExecuteProperties().getNextWorks().put(EndWork.END_WORK_ID, nextWorkWrapper);
                    endWork.getWorkExecuteProperties().getPreWorks().put(work.getId(), work);
                }
            }

            //build reference
            for (FeignFlow feignFlow : feignFlows) {
                NextFeignFlow[] nextFeignFlows = feignFlow.nextFeignFlows();
                if (nextFeignFlows.length != 0){
                    DefaultWork currentWork = allWorkMap.get(feignFlow.feignFlowId());
                    DefaultWork nextWork = null;
                    for (NextFeignFlow nextFeignFlow : nextFeignFlows) {
                        String nextWorkId = nextFeignFlow.feignFlowId();
                        nextWork = allWorkMap.get(nextWorkId);
                        NextWorkWrapper nextWorkWrapper = NextWorkWrapper.build(nextWork, nextFeignFlow.necessary(), getCondition(nextFeignFlow.conditionClass()));
                        currentWork.getWorkExecuteProperties().getNextWorks().put(nextWorkId, nextWorkWrapper);
                        nextWork.getWorkExecuteProperties().getPreWorks().put(currentWork.getId(), currentWork);
                        startWorkMap.remove(nextWorkId);
                    }
                }
            }

            startWorkMap.forEach((workId, work) -> {
                NextWorkWrapper nextWorkWrapper = NextWorkWrapper.build(work, true, getCondition(SimpleCondition.class));
                initWork.getWorkExecuteProperties().getNextWorks().put(workId, nextWorkWrapper);
                work.getWorkExecuteProperties().getPreWorks().put(InitWork.INIT_WORK_ID, initWork);
            });
            WorkExecuteDesigner designer = WorkExecuteDesigner.builder().initWork(initWork).endWork(endWork).build();
            designerCache.put(method, designer);
        }
        return designerCache.get(method);
    }
}
